using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Linq;


namespace GuavaData
{
    public class HSplit : Transform
    {
        string t; //original table
        List<string> cin; // a list that stores the domain of the split column
        string cin1; // split column
        // Fields for run-time state of the columns
        Dictionary<string, Domain.Domain> inputColumns; // dictionary storing all the columns and their domain values 
        List<string> inputKeys; // list of the key columns including cin1

        public HSplit(string table, string cin1)
        {
            this.t = table;
            this.cin = new List<string>();
            this.cin1 = cin1;

            // Until the schema is pushed through, do not assign to values in memory
            this.inputColumns = null;
            this.inputKeys = null;
        }

        // cin - list of values in the split column
        // col - each item in cin
        internal override void Expand(AddColumn ac, Command c)
        {
            // If the table doesn't match, punt
            if (ac.Table != t)
                return;

            // If the column already exists, throw error
            if (this.inputColumns.ContainsKey(ac.Column))
                throw new Exception("Column " + ac.Column + " already exists in table " + ac.Table + ".");

            else if (!this.inputColumns.ContainsKey(ac.Column))
            {
                List<Command> newstatements = new List<Command>();

                // for each value in the split column, add the new column
                foreach (string col in cin)
                {
                    Command acnew = new Command(new AddColumn(col, ac.Column, ac.Domain));
                    newstatements.Add(acnew);
                }

                this.inputColumns.Add(ac.Column, ac.Domain); // update the input columns
                c.NotifyChange(newstatements.ToArray()); // notify the change
            }
        }


        internal override void Expand(DropColumn dc, Command c)
        {
            // If the table doesn't match, punt
            if (dc.Table != t)
                return;

            // If the column does not exist, throw error
            if (!(this.inputColumns.ContainsKey(dc.Column)))
                throw new Exception("Column " + dc.Column + " does not exist in table " + dc.Table + ".");

            else if (!this.inputColumns.ContainsKey(dc.Column))
            {
                List<Command> oldst = new List<Command>();

                // drop the column from each split table
                foreach (string col in cin)
                {
                    Command dcnew = new Command(new DropColumn(col, dc.Column));
                    oldst.Add(dcnew);

                }

                // if the column is a key column, remove from the input keys list.
                if (this.inputKeys.Contains(dc.Column))
                {
                    this.inputKeys.Remove(dc.Column);
                }
                // Remove the dropped column from input columns
                this.inputColumns.Remove(dc.Column);
                c.NotifyChange(oldst.ToArray()); // notify change
            }
        }


        internal override void Expand(DropTable dt, Command c)
        {  // If the table doesn't match, punt
            if (dt.Table != t)
                return;

            // clear the state information
            this.inputColumns.Clear();
            this.inputColumns = null;
            this.inputKeys.Clear();
            this.inputKeys = null;


            List<Command> dstat = new List<Command>();

            //drop all the split tables
            foreach (string col in cin)
            {
                Command dtab = new Command(new DropTable(col));
                dstat.Add(dtab);

            }
            c.NotifyChange(dstat.ToArray()); // notify change

        }


        internal override void Expand(AlterColumnRename acr, Command c)
        {
            // If the table doesn't match, punt
            if (acr.Table != t)
                return;

            List<Command> rencol = new List<Command>();

            // change the column name in each of the split table
            foreach (string col in cin)
            {
                Command acrnew = new Command(new AlterColumnRename(col, acr.OldName, acr.NewName));
                rencol.Add(acrnew);

            }

            c.NotifyChange(rencol.ToArray()); // notify change

            Dictionary<string, Domain.Domain> newlist = new Dictionary<string, Domain.Domain>();

            // update the changes made to a new dictionary
            foreach (KeyValuePair<string, Domain.Domain> p in this.inputColumns)
            {
                if (p.Key != acr.OldName)// if the column name has not been changed
                {
                    newlist.Add(p.Key, p.Value);
                }
                else
                {
                    newlist.Add(acr.NewName, p.Value); // if the column name has been changed
                }
            }
            // clear the input columns and add the values from the newlist to the input columns
            this.inputColumns.Clear();
            this.inputColumns = newlist;

            // make changes to the inputkeys if the key column has been renamed
            if (this.inputKeys.Contains(acr.OldName))
            {
                inputKeys.Remove(acr.OldName);
                inputKeys.Add(acr.NewName);
            }
        }


        internal override void Expand(AddElement ae, Command c)
        {
            // If the table doesn't match, punt
            if (ae.Table != t)
                return;

            // if the domain already contains the element
            Domain.Enumerated e = this.inputColumns[ae.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(ae.Element);
            if (i >= 0)
                return;//Don't freak out... throw new Exception("Domain already contains the element " + ae.Element + ".");

            // if the element is to be added in the split column, then create a new table for that element
            if (ae.Column == cin1)
            {
                Dictionary<string, Domain.Domain> temp = new Dictionary<string, GuavaData.Domain.Domain>(this.inputColumns);
                temp.Remove(cin1);

                AddTable atab = new AddTable(ae.Element);
                foreach (KeyValuePair<string, Domain.Domain> p in temp)
                {
                    if (this.inputKeys.Contains(p.Key))
                    {
                        atab.AddColumn(p.Key, p.Value, true);

                    }
                    else
                    {
                        atab.AddColumn(p.Key, p.Value, false);
                    }
                }
                c.NotifyChange(new Command(atab));
                cin.Add(ae.Element);



            }
            // if the element is to be added in any other column, then add the element in each of the split tables.
            else
            {
                List<Command> adel = new List<Command>();
                foreach (string col in cin)
                {
                    Command addel = new Command(new AddElement(col, ae.Column, ae.Element));
                    adel.Add(addel);

                }
                c.NotifyChange(adel.ToArray());
            }

            // add the element to the domain of the column in the input columns dictionary.
            Domain.Enumerated enew = new GuavaData.Domain.Enumerated(e.Items);
            enew.Items.Add(ae.Element);
            this.inputColumns[ae.Column] = enew;
        }


        // Adding a new table
        internal override void Expand(AddTable at, Command c)
        {

            // If the table doesn't match, punt
            if (at.Table != t)
                return;

            // If the table already exists, throw an error - table exists if the dictionary of columns is present
            if (this.inputColumns != null)
                throw new Exception("Table " + at.Table + " already exists.");

            this.inputColumns = new Dictionary<string, GuavaData.Domain.Domain>();
            this.inputKeys = new List<string>();

            // get the position of the split column in order to create the child tables
            int a = at.Columns.IndexOf(cin1);
            Domain.Domain dom1 = at.Domains[a];
            Domain.Enumerated e = dom1 as Domain.Enumerated;

            // load the list 'cin' with the values from the split column 
            for (int i = 0; i < e.Items.Count; i++)
            {
                cin.Add(e.Items[i].ToString());
            }

            List<Command> atb = new List<Command>();

            // for each of the values in the split column, create a new table
            foreach (string col in cin)
            {
                AddTable atab = new AddTable(col);

                for (int i = 0; i < at.Columns.Count; i++)
                {
                    if (at.Columns[i] != cin1)
                    {
                        if (!at.Keys.Contains(at.Columns[i]))
                        {
                            atab.AddColumn(at.Columns[i], at.Domains[i], false);
                            //atb.Add(new Command(atab));
                        }
                        else
                        {
                            atab.AddColumn(at.Columns[i], at.Domains[i], true);
                            //atb.Add(new Command(atab));
                        }
                    }
                }
                atb.Add(new Command(atab));
            }

            // update the state information
            for (int i = 0; i < at.Columns.Count; i++)
            {
                this.inputColumns.Add(at.Columns[i], at.Domains[i]);
            }

            this.inputKeys = at.Keys; // load the input keys list with the list of keys.

            c.NotifyChange(atb.ToArray());
        }


        internal override void Expand(DropElement de, Command c)
        {
            // If the table doesn't match, punt
            if (de.Table != t)
                return;

            List<Command> dropele2 = new List<Command>();

            // if the domain does not contain the element, throw an exception
            Domain.Enumerated e = this.inputColumns[de.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(de.Element);
            if (i < 0)
                throw new Exception("Domain does not contain the element " + de.Element + ".");

            // if the element to be deleted is in the split column, delete the child table corresponding to that value
            if (de.Column == cin1)
            {
                c.NotifyChange(new Command(new DropTable(de.Element)));
                cin.Remove(de.Element);

            }
            // otherwise, delete the element from all the child tables
            else
            {
                foreach (string col in cin)
                {
                    Command dropel2 = new Command(new DropElement(col, de.Column, de.Element));
                    dropele2.Add(dropel2);
                }
                c.NotifyChange(dropele2.ToArray());
            }

            // update the domain 
            Domain.Enumerated enew = new GuavaData.Domain.Enumerated(e.Items);
            enew.Items.RemoveAt(i);
            this.inputColumns[de.Column] = enew;
        }

        // renaming the element
        internal override void Expand(AlterElementRename aer, Command c)
        {
            // If the table doesn't match, punt
            if (aer.Table != t)
                return;

            // If the domain does not contain the element, throw an exception
            Domain.Enumerated e = this.inputColumns[aer.Column] as Domain.Enumerated;
            int i = e.Items.IndexOf(aer.OldName);
            if (i < 0)
                throw new Exception("Domain does not contain the element " + aer.OldName + ".");



            List<Command> aernew2 = new List<Command>();

            // if the element is in the split column, change the name of the child table
            if (aer.Column == cin1)
            {
                c.NotifyChange(new Command(new AlterTableRename(aer.OldName, aer.NewName)));
                cin.Remove(aer.OldName);
                cin.Add(aer.NewName);

            }
            // otherwise, rename the element in all the child tables
            else
            {
                foreach (string col in cin)
                {
                    Command aer2 = new Command(new AlterElementRename(col, aer.Column, aer.OldName, aer.NewName));
                    aernew2.Add(aer2);

                }
                c.NotifyChange(aernew2.ToArray());
            }

            // update the input columns
            Domain.Enumerated enew = new GuavaData.Domain.Enumerated(e.Items);
            enew.Items.RemoveAt(i);
            enew.Items.Insert(i, aer.NewName);
            this.inputColumns[aer.Column] = enew;

        }


        internal override void Expand(Insert i, Command c)
        {
            // If the table doesn't match, punt
            if (i.Table != t)
                return;


            // Process the child
            i.Child.Visit(this, c);
            // if it is a row constant, 
            if (i.Child is RowConstant)
            {
                int a = (i.Child as RowConstant).exposedColumns.IndexOf(cin1);
                (i.Child as RowConstant).exposedColumns.RemoveAt(a); ;
                (i.Child as RowConstant).exposedDomains.RemoveAt(a);
                string rcin = (i.Child as RowConstant).Fields[a].ToString();
                (i.Child as RowConstant).Fields.RemoveAt(a);
                i.Columns.Remove(cin1);
                Insert ins = new Insert(rcin, i.Columns, i.Child);
                c.NotifyChange(new Command(ins));
            }
            else
            // if the child is a query, then insert into all child tables
            {
                List<Command> com = new List<Command>();
                foreach (string col in cin)
                {
                    i.Columns.Remove(cin1);
                    Filter fil = new Filter(i.Child);
                    fil.AddCondition(cin1, Condition.NewCondition(ConditionType.EQ, col));
                    Project proj = new Project(fil, i.Columns);
                    Insert ins = new Insert(col, i.Columns, proj);
                    com.Add(c.CloneState(ins));
                }

                c.NotifyChange(com.ToArray());
            }
        }


        // Table rename, no change
        internal override void Expand(AlterTableRename atr, Command c)
        {
            Command rename = new Command(new AlterTableRename(atr.OldName, atr.NewName));
            c.NotifyChange();

        }

        internal override void Expand(Update u, Command c)
        {
            // If the table doesn't match, punt
            if (u.Table != t)
                return;
            u.Child.Visit(this, c);

            // if the split column is one of the condition column, then update the child table corresponding to the value in that condition 
            if (u.CondColumns.Contains(cin1))
            {

                int j = u.CondColumns.IndexOf(cin1);


                String a = u.Conditions[j].Args[0].ToString();


                List<Condition> newcond = new List<Condition>(u.Conditions);

                newcond.Remove(u.Conditions[j]);

                List<string> newcondcol = new List<string>(u.CondColumns);
                newcondcol.Remove(cin1);

                Update upd = new Update(a, newcondcol, newcond, u.Columns, u.Child);
                c.NotifyChange(new Command(upd));

            }
            // otherwise, do the updation in all the child tables
            else
            {
                List<Command> up = new List<Command>();
                foreach (string col in cin)
                {
                    Update upd1 = new Update(col, u.CondColumns, u.Conditions, u.Columns, u.Child);
                    up.Add(new Command(upd1));
                }
                c.NotifyChange(up.ToArray());
            }
        }

        internal override void Expand(Delete d, Command c)
        {
            // If the table doesn't match, punt
            if (d.Table != t)
                return;

            // if the delete condition contains the split column, delete from the child table corresponding to the value in the delete condition
            if (d.Columns.Contains(cin1))
            {

                int j = d.Columns.IndexOf(cin1);


                String a = d.Conditions[j].Args[0].ToString();


                List<Condition> newcond = new List<Condition>(d.Conditions);


                newcond.Remove(d.Conditions[j]);

                List<string> newcondcol = new List<string>(d.Columns);


                newcondcol.Remove(cin1);

                Delete del = new Delete(a, newcondcol, newcond);

                c.NotifyChange(new Command(del));

            }
            // otherwise, delete from all child tables
            else
            {
                List<Command> del1 = new List<Command>();
                foreach (string col in cin)
                {
                    Delete dl = new Delete(col, d.Columns, d.Conditions);
                    del1.Add(new Command(dl));
                }
                c.NotifyChange(del1.ToArray());
            }

        }


        internal override void Expand(ForeignKey fk, Command c)
        { // if both the tables are not t, then return
            if ((fk.ChildTable != t) && (fk.ParentTable != t))
            {
                return;
            }
            // if child table is t and if the foreign key has a condition for the split column
            if ((fk.ChildTable == t) && (fk.ChildConditionColumns.Contains(cin1)))
            {
                int i = fk.ChildConditionColumns.IndexOf(cin1);
                Condition x = fk.ChildConditions[i];
                String a = x.Args[0].ToString();
                fk.ChildColumns.Remove(cin1);
                fk.ChildConditionColumns.Remove(cin1);
                fk.ChildConditions.Remove(x);
                fk.ParentColumns.Remove(cin1);
                ForeignKey fkey = new ForeignKey(a, fk.ChildConditionColumns, fk.ChildConditions, fk.ChildColumns, fk.ParentTable, fk.ParentConditionColumns, fk.ParentConditions, fk.ParentColumns);
                c.NotifyChange(new Command(fkey));
            }
            // if the child table is t
            // if the foreign key doesn't include a condition for the split column
            else if ((fk.ChildTable == t) && (!fk.ChildConditionColumns.Contains(cin1)))
            {
                fk.ChildColumns.Remove(cin1);
                fk.ParentColumns.Remove(cin1);
                List<Command> fkcom = new List<Command>();
                foreach (string col in cin)
                {
                    ForeignKey fkey = new ForeignKey(col, fk.ChildConditionColumns, fk.ChildConditions, fk.ChildColumns, fk.ParentTable, fk.ParentConditionColumns, fk.ParentConditions, fk.ParentColumns);
                    fkcom.Add(new Command(fkey));
                }
                c.NotifyChange(fkcom.ToArray());
            }
            // if parent table is t and if the foreign key has a condition for the split column

            else if ((fk.ParentTable == t) && (fk.ParentConditionColumns.Contains(cin1)))
            {
                fk.ChildColumns.Remove(cin1);
                int i = fk.ParentConditionColumns.IndexOf(cin1);
                Condition x = fk.ParentConditions[i];
                String a = x.Args[0].ToString();
                fk.ParentColumns.Remove(cin1);
                fk.ParentConditionColumns.Remove(cin1);
                fk.ParentConditions.Remove(x);
                ForeignKey fkey = new ForeignKey(fk.ChildTable, fk.ChildConditionColumns, fk.ChildConditions, fk.ChildColumns, a, fk.ParentConditionColumns, fk.ParentConditions, fk.ParentColumns);
                c.NotifyChange(new Command(fkey));
            }
            // if parent table is t and if the foreign key has no condition for the split column, drop statement
            else if ((fk.ParentTable == t) && (!fk.ParentConditionColumns.Contains(cin1)))
                c.NotifyChange();
        }


        // Table reference for HSplit
        internal override void Expand(TableRef tr, Command c)
        {
            if (tr.Table != this.t)
                return;

            //get the domain of the split column
            Domain.Domain dom1 = this.inputColumns[cin1];
            Domain.Enumerated e = dom1 as Domain.Enumerated;
            List<string> dom = new List<string>(e.Items);
            List<Operator> crossOper = new List<Operator>();

            for (int j = 0; j < dom.Count; j++)
            {
                RowConstant rc = new RowConstant(new string[] { cin1 }, new Domain.Domain[] { dom1 }, new string[] { dom[j] });
                string newAlias = c.nextTableAlias(dom[j]);
                TableRef right = new TableRef(dom[j], newAlias);

                for (int i = 0; i < tr.Columns.Count; i++)
                {
                    if (tr.Columns[i] != cin1)
                    {
                        right.AddColumn(tr.Columns[i], tr.exposedColumns[i], tr.exposedDomains[i]);
                    }
                }
                CrossJoin cross = new CrossJoin(right, rc);
                crossOper.Add(cross);
            }
            Union unitab = new Union(crossOper);
            tr.parent.NotifyChange(tr, unitab);
        }
        public override GuavaData.Transform Inverse()
        {
            HMerge hm = new HMerge(cin, t, cin1);
            return hm;
        }
        public override List<XElement> ToXElement()
        {
            XElement x = new XElement("Transform", new XAttribute("type","HSplit"), new XAttribute("table", t), new XAttribute("column", cin1));
            List<XElement> xList = new List<XElement>();
            if (!(this.Child is SqlProvider))
                xList = ((Transform)this.Child).ToXElement();
            xList.Add(x);
            return xList;
        }
        new public static HSplit FromXElement(XElement x)
        {
            string table = x.Attribute("table").Value;
            string column = x.Attribute("column").Value;
            if (!(table == null) && !(column == null))
                return new HSplit(table, column);
            else
                throw new Exception("While loading HSplit from XML: table or column was null");
        }
    }
}
   





